{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "## Открытый курс по машинному обучению\n",
    "</center>\n",
    "Автор материала: программист-исследователь Mail.ru Group, старший преподаватель Факультета Компьютерных Наук ВШЭ Юрий Кашницкий. Материал распространяется на условиях лицензии [Creative Commons CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/). Можно использовать в любых целях (редактировать, поправлять и брать за основу), кроме коммерческих, но с обязательным упоминанием автора материала."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# <center>Тема 4. Линейные модели классификации и регрессии\n",
    "## <center>Часть 4. Где логистическая регрессия хороша и где не очень"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Анализ отзывов IMDB к фильмам"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Будем решать задачу бинарной классификации отзывов IMDB к фильмам. Имеется обучающая выборка с размеченными отзывами, по 12500 отзывов известно, что они хорошие, еще про 12500 – что они плохие. Здесь уже не так просто сразу приступить к машинному обучению, потому что готовой матрицы $X$ нет  – ее надо приготовить. Будем использовать самый простой подход – мешок слов (\"Bag of words\"). При таком подходе признаками отзыва будут индикаторы наличия в нем каждого слова из всего корпуса, где корпус – это множество всех отзывов. Идея иллюстрируется картинкой\n",
    "\n",
    "<img src=\"../../img/bag_of_words.svg\" width=80%>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from __future__ import division, print_function\n",
    "\n",
    "# отключим всякие предупреждения Anaconda\n",
    "import warnings\n",
    "\n",
    "warnings.filterwarnings(\"ignore\")\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import seaborn as sns\n",
    "from sklearn.datasets import load_files\n",
    "from sklearn.feature_extraction.text import (\n",
    "    CountVectorizer,\n",
    "    TfidfTransformer,\n",
    "    TfidfVectorizer,\n",
    ")\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.svm import LinearSVC"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Загрузим данные [отсюда](http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz) (это прямая ссылка на скачивание, а [вот](http://ai.stanford.edu/~amaas/data/sentiment/) описание набора данных). В обучающей и тестовой выборках по 12500 тысяч хороших и плохих отзывов к фильмам.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# поменяйте путь к файлу\n",
    "reviews_train = load_files(\n",
    "    \"/Users/y.kashnitsky/Yandex.Disk.localized/ML/data/aclImdb/train\",\n",
    "    categories=[\"pos\", \"neg\"],\n",
    ")\n",
    "text_train, y_train = reviews_train.data, reviews_train.target"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Number of documents in training data: %d\" % len(text_train))\n",
    "print(np.bincount(y_train))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# поменяйте путь к файлу\n",
    "reviews_test = load_files(\n",
    "    \"/Users/y.kashnitsky/Yandex.Disk.localized/ML/data/aclImdb/test\"\n",
    ")\n",
    "text_test, y_test = reviews_test.data, reviews_test.target\n",
    "print(\"Number of documents in test data: %d\" % len(text_test))\n",
    "print(np.bincount(y_test))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Пример отзыва и соответствующей метки.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(text_train[1])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_train[1]  # плохой отзыв"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "text_train[2]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_train[2]  # хороший отзыв"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Простой подсчет слов"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Составим словарь всех слов с помощью CountVectorizer.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cv = CountVectorizer()\n",
    "cv.fit(text_train)\n",
    "\n",
    "len(cv.vocabulary_)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Посмотрим на примеры полученных \"слов\" (лучше их называть токенами). Видим, что многие важные этапы обработки текста мы тут пропустили.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(cv.get_feature_names()[:50])\n",
    "print(cv.get_feature_names()[50000:50050])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "**Закодируем предложения из текстов обучающей выборки индексами входящих слов. Используем разреженный формат.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train = cv.transform(text_train)\n",
    "X_train"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Посмотрим, как преобразование подействовало на одно из предложений.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(text_train[19726])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train[19726].nonzero()[1]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train[19726].nonzero()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Преобразуем так же тестовую выборку.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_test = cv.transform(text_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Обучим логистическую регрессию.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "logit = LogisticRegression(n_jobs=-1, random_state=7)\n",
    "logit.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Посмотрим на доли правильных ответов на обучающей и тестовой выборках.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "round(logit.score(X_train, y_train), 3), round(logit.score(X_test, y_test), 3),"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Коэффициенты модели можно красиво отобразить.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def visualize_coefficients(classifier, feature_names, n_top_features=25):\n",
    "    # get coefficients with large absolute values\n",
    "    coef = classifier.coef_.ravel()\n",
    "    positive_coefficients = np.argsort(coef)[-n_top_features:]\n",
    "    negative_coefficients = np.argsort(coef)[:n_top_features]\n",
    "    interesting_coefficients = np.hstack([negative_coefficients, positive_coefficients])\n",
    "    # plot them\n",
    "    plt.figure(figsize=(15, 5))\n",
    "    colors = [\"red\" if c < 0 else \"blue\" for c in coef[interesting_coefficients]]\n",
    "    plt.bar(np.arange(2 * n_top_features), coef[interesting_coefficients], color=colors)\n",
    "    feature_names = np.array(feature_names)\n",
    "    plt.xticks(\n",
    "        np.arange(1, 1 + 2 * n_top_features),\n",
    "        feature_names[interesting_coefficients],\n",
    "        rotation=60,\n",
    "        ha=\"right\",\n",
    "    );"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_grid_scores(grid, param_name):\n",
    "    plt.plot(\n",
    "        grid.param_grid[param_name],\n",
    "        grid.cv_results_[\"mean_train_score\"],\n",
    "        color=\"green\",\n",
    "        label=\"train\",\n",
    "    )\n",
    "    plt.plot(\n",
    "        grid.param_grid[param_name],\n",
    "        grid.cv_results_[\"mean_test_score\"],\n",
    "        color=\"red\",\n",
    "        label=\"test\",\n",
    "    )\n",
    "    plt.legend();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "visualize_coefficients(logit, cv.get_feature_names())"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Подберем коэффициент регуляризации для логистической регрессии. Используем `sklearn.pipeline`, поскольку `CountVectorizer` правильно применять только на тех данных, на которых в текущий момент обучается модель (чтоб не \"подсматривать\" в тестовую выборку и не считать по ней частоты вхождения слов). В данном случае `pipeline` задает последовательность действий: применить `CountVectorizer`, затем обучить логистическую регрессию.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "from sklearn.pipeline import make_pipeline\n",
    "\n",
    "text_pipe_logit = make_pipeline(\n",
    "    CountVectorizer(), LogisticRegression(n_jobs=-1, random_state=7)\n",
    ")\n",
    "\n",
    "text_pipe_logit.fit(text_train, y_train)\n",
    "print(text_pipe_logit.score(text_test, y_test))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "from sklearn.model_selection import GridSearchCV\n",
    "\n",
    "param_grid_logit = {\"logisticregression__C\": np.logspace(-5, 0, 6)}\n",
    "grid_logit = GridSearchCV(\n",
    "    text_pipe_logit, param_grid_logit, cv=3, n_jobs=-1, return_train_score=True\n",
    ")\n",
    "\n",
    "grid_logit.fit(text_train, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Лучшее значение C и соответствующее качество на кросс-валидации:**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "grid_logit.best_params_, grid_logit.best_score_"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_grid_scores(grid_logit, \"logisticregression__C\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На валидационной выборке:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "grid_logit.score(text_test, y_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Теперь то же самое, но со случайным лесом. Видим, что с логистической регрессией мы достигаем большей доли правильных ответов меньшими усилиями**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.ensemble import RandomForestClassifier"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "forest = RandomForestClassifier(n_estimators=200, n_jobs=-1, random_state=17)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "forest.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "round(forest.score(X_test, y_test), 3)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### XOR-проблема\n",
    "Теперь рассмотрим пример, где линейные модели справляются хуже. \n",
    "\n",
    "Линейные методы классификации строят все же очень простую разделяющую поверхность – гиперплоскость. Самый известный игрушечный пример, в котором классы нельзя без ошибок поделить гиперплоскостью (то есть прямой, если это 2D), получил имя \"the XOR problem\".\n",
    "\n",
    "XOR – это \"исключающее ИЛИ\", булева функция со следующей таблицей истинности:\n",
    "\n",
    "<img src='../../img/XOR_table.gif'>\n",
    "\n",
    "XOR дал имя простой задаче бинарной классификации, в которой классы представлены вытянутыми по диагоналям и пересекающимися облаками точек. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# порождаем данные\n",
    "rng = np.random.RandomState(0)\n",
    "X = rng.randn(200, 2)\n",
    "y = np.logical_xor(X[:, 0] > 0, X[:, 1] > 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.scatter(X[:, 0], X[:, 1], s=30, c=y, cmap=plt.cm.Paired);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Очевидно, нельзя провести прямую так, чтобы без ошибок отделить один класс от другого. Поэтому логистическая регрессия плохо справляется с такой задачей. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def plot_boundary(clf, X, y, plot_title):\n",
    "    xx, yy = np.meshgrid(np.linspace(-3, 3, 50), np.linspace(-3, 3, 50))\n",
    "    clf.fit(X, y)\n",
    "    # plot the decision function for each datapoint on the grid\n",
    "    Z = clf.predict_proba(np.vstack((xx.ravel(), yy.ravel())).T)[:, 1]\n",
    "    Z = Z.reshape(xx.shape)\n",
    "\n",
    "    image = plt.imshow(\n",
    "        Z,\n",
    "        interpolation=\"nearest\",\n",
    "        extent=(xx.min(), xx.max(), yy.min(), yy.max()),\n",
    "        aspect=\"auto\",\n",
    "        origin=\"lower\",\n",
    "        cmap=plt.cm.PuOr_r,\n",
    "    )\n",
    "    contours = plt.contour(xx, yy, Z, levels=[0], linewidths=2, linetypes=\"--\")\n",
    "    plt.scatter(X[:, 0], X[:, 1], s=30, c=y, cmap=plt.cm.Paired)\n",
    "    plt.xticks(())\n",
    "    plt.yticks(())\n",
    "    plt.xlabel(r\"$x_1$\")\n",
    "    plt.ylabel(r\"$x_2$\")\n",
    "    plt.axis([-3, 3, -3, 3])\n",
    "    plt.colorbar(image)\n",
    "    plt.title(plot_title, fontsize=12);"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_boundary(LogisticRegression(), X, y, \"Logistic Regression, XOR problem\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "А вот если на вход подать полиномиальные признаки, в данном случае до 2 степени, то проблема решается. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.preprocessing import PolynomialFeatures"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logit_pipe = Pipeline(\n",
    "    [(\"poly\", PolynomialFeatures(degree=2)), (\"logit\", LogisticRegression())]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plot_boundary(logit_pipe, X, y, \"Logistic Regression + quadratic features. XOR problem\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Здесь логистическая регрессия все равно строила гиперплоскость, но в 6-мерном пространстве признаков $1, x_1, x_2, x_1^2, x_1x_2$ и $x_2^2$. В проекции на исходное пространство признаков $x_1, x_2$ граница получилась нелинейной. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "На практике полиномиальные признаки действительно помогают, но строить их явно – вычислительно неэффективно. Гораздо быстрее работает SVM с ядровым трюком. При таком подходе в пространстве высокой размерности считается только расстояние между объектами (задаваемое функцией-ядром), а явно плодить комбинаторно большое число признаков не приходится. Про это подробно можно почитать в курсе Евгения Соколова – [тут](https://github.com/esokolov/ml-course-msu/blob/master/ML16/lecture-notes/Sem10_linear.pdf) (математика уже серьезная)."
   ]
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.1"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 1
}
